/*
 * This file is part of Cadmium.
 * Copyright (C) 2007-2010 Xavier Clerc.
 *
 * Cadmium is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Cadmium is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package fr.x9c.cadmium.primitives.dbm;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

import fr.x9c.cadmium.kernel.CodeRunner;
import fr.x9c.cadmium.kernel.Context;
import fr.x9c.cadmium.kernel.Fail;
import fr.x9c.cadmium.kernel.FalseExit;
import fr.x9c.cadmium.kernel.Fatal;
import fr.x9c.cadmium.kernel.Primitive;
import fr.x9c.cadmium.kernel.PrimitiveProvider;
import fr.x9c.cadmium.kernel.Value;
import fr.x9c.cadmium.util.CustomClassLoader;

/**
 * Implements all primitives from 'cldbm.c'.
 *
 * @author <a href="mailto:cadmium@x9c.fr">Xavier Clerc</a>
 * @version 1.1
 * @since 1.1
 */
@PrimitiveProvider
public final class Cldbm {

    /** Slot identifier for the actual Dbm implementation. */
    private static final Object SLOT = new Object();

    /** Path to jdbm implementation stream. */
    private static final String JDBM_STREAM = "/fr/x9c/cadmium/primitives/dbm/JdbmImplementation.clazz";

    /** Fully-qualified class name for jdbm implementation. */
    private static final String JDBM_CLASS = "fr.x9c.cadmium.primitives.dbm.JdbmImplementation";

    /** Class for the jdbm-based implementation. */
    private static Class jdbmClass = null;

    /**
     * No instance of this class.
     */
    private Cldbm() {
    } // end empty constructor

    /**
     * Opens a database.
     * @param ctxt context
     * @param file database file
     * @param flags allowed database operations
     * @param mode file permissions for created file
     * @return database handle
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if the file cannot be
     *                        opened/created
     * @throws Fatal.Exception if an implementation cannot be created
     */
    @Primitive
    public static Value caml_dbm_open(final CodeRunner ctxt,
                                      final Value file,
                                      final Value flags,
                                      final Value mode)
        throws Fail.Exception, Fatal.Exception, FalseExit {
        return getImpl(ctxt).open(ctxt, file, flags, mode);
    } // end method 'caml_dbm_open(CodeRunner, Value, Value, Value)'

    /**
     * Closes a database.
     * @param ctxt context
     * @param db database handle
     * @return <i>unit</i>
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if the database is already
     *                        closed
     * @throws Fatal.Exception if an implementation cannot be created
     */
    @Primitive
    public static Value caml_dbm_close(final CodeRunner ctxt,
                                       final Value db)
        throws Fail.Exception, Fatal.Exception, FalseExit {
        return getImpl(ctxt).close(ctxt, db);
    } // end method 'caml_dbm_close(CodeRunner, Value)'

    /**
     * Gets a value using its key.
     * @param ctxt context
     * @param db database handle
     * @param key key
     * @return data associated with passed key
     * @throws Fail.Exception <i>Not_found</i> if no entry exists for the
     *                        passed key
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if the database is already
     *                        closed
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if an i/o error occurs
     * @throws Fatal.Exception if an implementation cannot be created
     */
    @Primitive
    public static Value caml_dbm_fetch(final CodeRunner ctxt,
                                       final Value db,
                                       final Value key)
        throws Fail.Exception, Fatal.Exception, FalseExit {
        return getImpl(ctxt).fetch(ctxt, db, key);
    } // end method 'caml_dbm_fetch(CodeRunner, Value, Value)'

    /**
     * Inserts a (key, data) pair in a database.
     * @param ctxt context
     * @param db database handle
     * @param key key
     * @param content data associated with key
     * @return <i>unit</i>
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if the database is already
     *                        closed
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if an i/o error occurs
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if an entry with the same
     *                        key already exists
     * @throws Fatal.Exception if an implementation cannot be created
     */
    @Primitive
    public static Value caml_dbm_insert(final CodeRunner ctxt,
                                        final Value db,
                                        final Value key,
                                        final Value content)
        throws Fail.Exception, Fatal.Exception, FalseExit {
        return getImpl(ctxt).insert(ctxt, db, key, content);
    } // end method 'caml_dbm_insert(CodeRunner, Value, Value, Value)'

    /**
     * Inserts a (key, data) pair in a database, replacing current value
     * if a pair with same key already exists.
     * @param ctxt context
     * @param db database handle
     * @param key key
     * @param content data associated with key
     * @return <i>unit</i>
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if the database is already
     *                        closed
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if an i/o error occurs
     * @throws Fatal.Exception if an implementation cannot be created
     */
    @Primitive
    public static Value caml_dbm_replace(final CodeRunner ctxt,
                                         final Value db,
                                         final Value key,
                                         final Value content)
        throws Fail.Exception, Fatal.Exception, FalseExit {
        return getImpl(ctxt).replace(ctxt, db, key, content);
    } // end method 'caml_dbm_replace(CodeRunner, Value, Value, Value)'

    /**
     * Deletes a value using its key.
     * @param ctxt context
     * @param db database handle
     * @param key key
     * @return <i>unit</i>
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if the database is already
     *                        closed
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if an i/o error occurs
     * @throws Fatal.Exception if an implementation cannot be created
     */
    @Primitive
    public static Value caml_dbm_delete(final CodeRunner ctxt,
                                        final Value db,
                                        final Value key)
        throws Fail.Exception, Fatal.Exception, FalseExit {
        return getImpl(ctxt).delete(ctxt, db, key);
    } // end method 'caml_dbm_delete(CodeRunner, Value, Value)'

    /**
     * Returns the first key of a database.
     * @param ctxt context
     * @param db database handle
     * @return the first key of the passed database
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if the database is already
     *                        closed
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if such a key does not
     *                        exist
     * @throws Fatal.Exception if an implementation cannot be created
     */
    @Primitive
    public static Value caml_dbm_firstkey(final CodeRunner ctxt,
                                          final Value db)
        throws Fail.Exception, Fatal.Exception {
        return getImpl(ctxt).firstkey(ctxt, db);
    } // end method 'caml_dbm_firstkey(CodeRunner, Value)'

    /**
     * Returns the next key of a database.
     * @param ctxt context
     * @param db database handle
     * @return the next key of the passed database
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if the database is already
     *                        closed
     * @throws Fail.Exception <i>Dbm.Dbm_error</i> if such a key does not
     *                        exist
     * @throws Fatal.Exception if an implementation cannot be created
     */
    @Primitive
    public static Value caml_dbm_nextkey(final CodeRunner ctxt,
                                         final Value db)
        throws Fail.Exception, Fatal.Exception {
        return getImpl(ctxt).nextkey(ctxt, db);
    } // end method 'caml_dbm_nextkey(CodeRunner, Value)'

    /**
     * Returns the actual implementation for the Dbm module.
     * @param ctxt context - should not be <tt>null</tt>
     * @return the actual implementation for the Dbm module
     * @throws Fatal.Exception if an implementation cannot be created
     */
    private static DbmImplementation getImpl(final CodeRunner ctxt)
        throws Fatal.Exception {
        assert ctxt != null : "null ctxt";
        final Context c = ctxt.getContext();
        final DbmImplementation impl = (DbmImplementation) c.getSlot(Cldbm.SLOT);
        if (impl != null) {
            return impl;
        } else {
            final DbmImplementation newImpl = c.getParameters().isJDBMUsed()
                ? newJdbmImplementation()
                : new BasicImplementation();
            c.registerSlot(Cldbm.SLOT, newImpl);
            return newImpl;
        } // end if/else
    } // end method 'getImpl(CodeRunner)'

    /**
     * Creates a JDBM-based implementation using reflection.
     * @return a new JDBM-based implementation
     * @throws Fail.Exception if an implementation cannot be created
     */
    private static DbmImplementation newJdbmImplementation()
        throws Fatal.Exception {
        synchronized (Cldbm.class) {
            if (Cldbm.jdbmClass == null) {
                try {
                    final InputStream is = Cldbm.class.getResourceAsStream(Cldbm.JDBM_STREAM);
                    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int b = is.read();
                    while (b != -1) {
                        baos.write(b);
                        b = is.read();
                    } // end while
                    Cldbm.jdbmClass =
                        CustomClassLoader.INSTANCE.defineAndResolve(Cldbm.JDBM_CLASS,
                                                                    baos.toByteArray());
                } catch (final Throwable t) {
                    Fatal.raise("Cannot create a JDBM implementation");
                    return null; // never reached
                } // end try/catch
            } // end if
        } // end synchronized
        if (Cldbm.jdbmClass == null) {
            Fatal.raise("Cannot create a JDBM implementation");
        } // end if
        try {
            return (DbmImplementation) Cldbm.jdbmClass.newInstance();
        } catch (final InstantiationException ie) {
            Fatal.raise("Cannot create a JDBM implementation");
            return null; // never reached
        } catch (final IllegalAccessException iae) {
            Fatal.raise("Cannot create a JDBM implementation");
            return null; // never reached
        } // end try/catch
    } // end method 'newJdbmImplementation()'

    /**
     * Raises a <i>Dbm.Dbm_error</i> exception.
     * @param ctxt context - should not be <tt>null</tt>
     * @param msg exception message - should not be <tt>null</tt>
     * @throws Fail.Exception always
     */
    public static void error(final CodeRunner ctxt,
                             final String msg)
        throws Fail.Exception {
        assert ctxt != null : "null ctxt";
        assert msg != null : "null msg";
        final Value dbmException = ctxt.getContext().getCallback("dbmerror");
        if (dbmException == null) {
            Fail.invalidArgument("Dbm.Dbm_error is not initialized");
        } else {
            Fail.raiseWithString(dbmException, msg);
        } // end if/else
    } // end method 'error(CodeRunner, String)'

} // end class 'Cldbm'
